#########################################################
##
##
## OpenGl Canvas for drawing stereo 3D images and
## handling mouse and keyboard control
##
##
#########################################################

import wx
import traceback


from OpenGL.GL               import *
from OpenGL.GLU              import *
from OpenGL.GLUT             import *
from wx.glcanvas             import GLCanvas as wxGLCanvas
from wx.glcanvas             import WX_GL_RGBA, WX_GL_DOUBLEBUFFER
from math                    import pi, exp
from random                  import random, randint
from time                    import time

from colors                  import COLOR_BLACK, COLOR_GREY, COLOR_WHITE, GEN_GREY
from timestat                import timestat
from logger                  import log
from logging                 import DEBUG, INFO, WARN, ERROR, CRITICAL
from camera                  import Camera
from objectregistry          import objectRegistry
from gobjects                import Sphere, GObject, Ground
from fonts                   import fonts, tooltipFont
from testscene               import TestSceneGenerator, TestScene, TestOverlay
from settings                import settings
#########################################################
##
##
##  Constants
##
##
#########################################################

BACKGROUNDCOLOR      = (1, 1, 1, 1)
GROUND_TRANSPARENCY  = (0.8,)

logLevels = [DEBUG, INFO, WARN, ERROR, CRITICAL]
#########################################################
##
##
##  Classes
##
##
#########################################################


class Canvas3D(wxGLCanvas):
    def __init__(self, parent, colorMode, doubleBuffer, sceneGenerator=None):
        wxGLCanvas.__init__(self, parent,
            attribList=(colorMode, doubleBuffer,))

        self.initBindings()
        self.initVariables(parent, sceneGenerator)
        
    @timestat
    def initBindings(self):
        self.Bind(wx.EVT_PAINT, self.onPaint)
        self.Bind(wx.EVT_SIZE, self.onResize)
        self.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)
        self.Bind(wx.EVT_CLOSE, self.onClose)
        self.Bind(wx.EVT_MOTION, self.onMouseMotion)
        self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.onLeftUp)
        self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDClick)
        self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
        self.Bind(wx.EVT_RIGHT_UP, self.onRightUp)
        self.Bind(wx.EVT_MIDDLE_DOWN, self.onMiddleDown)
        self.Bind(wx.EVT_MIDDLE_UP, self.onMiddleUp)
        self.Bind(wx.EVT_MOUSEWHEEL, self.onMouseWheel)
        self.renderPickingTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onDrawPicking,
                  self.renderPickingTimer)
        self.renderPickingTimer.Stop()
        self.SetFocus()
    
    @timestat
    def initVariables(self, parent, sceneGenerator):
        self.settings = settings.canvas
        self.initialCameraState = (0,100,0,-pi/2,0)
        self.camera     = Camera(self, self.initialCameraState)
        self.camera.subscribe(self.onCameraChanged)
        
        self.init       = 0
        self.parent     = parent
        
        if sceneGenerator:
            self.scene   = sceneGenerator.get3DScene(self)
            self.overlay = sceneGenerator.getOverlayScene(self)
        else:
            testSG       = TestSceneGenerator()
            self.scene   = testSG.get3DScene(self)
            self.overlay = testSG.getOverlayScene(self)
        
        self.ground     = Ground(None, l=2000, w=2000, color=(COLOR_WHITE[:3] + (0.8,)),
                                 selectable=False)
        
        self.fovChanged = False
        
        self._leftDown  = False
        self._leftDownx = None
        self._leftDowny = None
        self._middleDown  = False
        self._middleDownx = None
        self._middleDowny = None
        self._rightDown   = False
        self._rightDownx  = None
        self._rightDowny  = None
        
        self.leftMouseDragStart   = False
        self.middleMouseDragStart = False
        self.rightMouseDragStart  = False
        self.lastMouseWheelTime   = time()
        
        self.latchedObject        = None
        self.oldLatchedObject     = None
        self.lastMouseMotionPosition = None
        self.highlightCallDelay   = 30
        
        self.cameraChanged = False
        self.logLevel = DEBUG
        log.setLevel(self.logLevel)
        
        self.pickingEnabled = False
        
        self.keyBindings = {('W',):[self.camera.moveForward],
                            ('S',):[self.camera.moveBackward],
                            ('A',):[self.camera.moveLeft],
                            ('D',):[self.camera.moveRight],
                            ('Q',):[self.camera.moveUp],
                            ('Z',):[self.camera.moveDown],
                            ('R',):[(lambda:self.camera.resetState(self.initialCameraState))],
                            ('G',):[self.camera.incMoveDistance],
                            ('F',):[self.incFov],
                            ('V',):[self.decFov],
                            
                            ('B',):[self.camera.decMoveDistance],
                            ('1',):[self.changeLogLevel],
                            ('CTRL', 314,):[lambda : self.camera.orbit(-1, 0)], # Left arrow
                            ('CTRL', 315,):[lambda : self.camera.orbit( 0, 1)], # Up arrow
                            ('CTRL', 316,):[lambda : self.camera.orbit( 1, 0)], # Right arrow
                            ('CTRL', 317,):[lambda : self.camera.orbit( 0,-1)], # Down arrow
                            ('F',):[self.incFov],
                            }
        
    def changeLogLevel(self):
        i = (logLevels.index(self.logLevel) + 1)%len(logLevels)
        self.logLevel = logLevels[i]
        log.setLevel(self.logLevel)
        log.critical('Changing log level to %d'%self.logLevel)

    def onLeftDown(self, event):
        assert isinstance(event, wx.MouseEvent)
        if self._middleDown:
            self.onMiddleUp(event)
        if self._rightDown:
            self.onRightUp(event)
        self._leftDown = True
        self._leftDownx, self._leftDowny = event.GetPositionTuple()
        ctrlDown  = 1 if event.ControlDown() else 0
        if ctrlDown:
            if self.latchedObject and self.latchedObject.selectable:
                log.debug('selecting latched object')
                self.selectedObject = self.latchedObject
            pass
        
        
    def onLeftUp(self, event):
        self._leftDown = False
        if self.leftMouseDragStart:
            self.leftMouseDragStart = False
            self.camera.leftDragEnd()
            wx.CallLater(self.highlightCallDelay, self.OnPickAndRefresh)
        else:
            # select object below 
            #if self.latchedObject and self.latchedObject.selectable:
                #print 'selecting latched object'
                #self.selectedObject = self.latchedObject
            pass
        
    def onLeftDClick(self, event):
        assert isinstance(event, wx.MouseEvent)
        
        
    def onRightDown(self, event):
        assert isinstance(event, wx.MouseEvent)
        if self._middleDown:
            self.onMiddleUp(event)
        if self._leftDown:
            self.onLeftUp(event)
        self._rightDown = True
        self._rightDownx, self._rightDowny = event.GetPositionTuple()
        
    def onRightUp(self, event):
        self._rightDown = False
        if self.rightMouseDragStart:
            self.rightMouseDragStart = False
            self.camera.rightDragEnd()
            wx.CallLater(self.highlightCallDelay, self.OnPickAndRefresh)
        
    def onMiddleDown(self, event):
        assert isinstance(event, wx.MouseEvent)
        if self._leftDown:
            self.onLeftUp(event)
        if self._rightDown:
            self.onRightUp(event)
        self._middleDown = True
        self._middleDownx, self._middleDowny = event.GetPositionTuple()
        
    def onMiddleUp(self, event):
        self._middleDown = False
        if self.middleMouseDragStart:
            self.middleMouseDragStart = False
            self.camera.middleDragEnd()
            wx.CallLater(self.highlightCallDelay, self.OnPickAndRefresh)
        
    @timestat
    def onMouseMotion(self, event):
        assert isinstance(event, wx.MouseEvent)
        posx, posy = event.GetPositionTuple()
        self.lastMouseMotionPosition = posx, posy, 0
        self.latchedObject = None
        
        ctrlDown  = 1 if event.ControlDown() else 0
        altDown   = 1 if event.AltDown() else 0
        shiftDown = 1 if event.ShiftDown() else 0
        metaDown  = 1 if event.MetaDown() else 0
        modifiers = (ctrlDown << 3) | (altDown << 2) | (shiftDown << 1) | (metaDown)
        
        if self._leftDown and not modifiers:
            dx, dy = posx - self._leftDownx, posy - self._leftDowny
            if (abs(dx) > 2) or (abs(dy) > 2):
                self.leftMouseDragStart = True
                self.camera.rotate(dx, dy)
        elif self._leftDown and (modifiers & 0x8):
            dx, dy = posx - self._leftDownx, posy - self._leftDowny
            if self.selectedObject:
                self.selectedObject.move(dx, dy, 0)
        elif self._middleDown:
            dx, dy = posx - self._middleDownx, posy - self._middleDowny
            if (abs(dx) > 2) or (abs(dy) > 2):
                self.middleMouseDragStart = True
                self.camera.orbit(dx, dy, self.latchedCoordinates)
        
        elif self._rightDown:
            dx, dy = posx - self._rightDownx, posy - self._rightDowny
            if (abs(dx) > 2) or (abs(dy) > 2):
                self.rightMouseDragStart = True
                self.camera.strafe(posx, self.windowSize[1] - posy, self.pickDepth,
                                   self.latchedCoordinates, self.latchedMVMatrix)
        
        elif self.pickingEnabled:
            sizey = self.windowSize[1]
            #log.debug('Mouse position = %d %d'%(posx, sizey - posy))
            oid, coords = self.pick(posx, sizey - posy)
            self.latchedObject = objectRegistry.getGObject(oid)
            if self.latchedObject == self.oldLatchedObject:
                if self.latchedObject:
                    self.latchedCoordinates = coords
                else:
                    self.latchedCoordinates = None
                return
            self.oldLatchedObject = self.latchedObject
            self.latchedMVMatrix  = glGetDoublev(GL_MODELVIEW_MATRIX)
            log.debug('tooltip = %s'%(self.latchedObject.tooltip
                                      if self.latchedObject else 'NA'))
            if self.latchedObject:
                self.latchedCoordinates = coords
            else:
                self.latchedCoordinates = None
            self.Refresh()
            
    def OnPickAndRefresh(self):
        if self.pickingEnabled:
            posx , posy, dummy = self.lastMouseMotionPosition
            sizey = self.windowSize[1]
            #log.debug('Mouse position = %d %d'%(posx, sizey - posy))
            oid, coords = self.pick(posx, sizey - posy)
            self.latchedObject = objectRegistry.getGObject(oid)
            if self.latchedObject:
                self.latchedCoordinates = coords
                if self.latchedObject == self.oldLatchedObject:
                    return
                self.oldLatchedObject = self.latchedObject
                self.latchedMVMatrix  = glGetDoublev(GL_MODELVIEW_MATRIX)
                log.debug('tooltip = %s'%(self.latchedObject.tooltip
                                          if self.latchedObject else 'NA'))
                self.Refresh()
            
    def onMouseWheel(self, event):
        wheelRotation = event.WheelRotation
        tnow = time()
        tdiff = tnow - self.lastMouseWheelTime
        self.lastMouseWheelTime = tnow
        scale = 40*exp(-20*tdiff) + 0.1
        log.info('wheel rotation scale: %4.2f'%scale)
        if wheelRotation > 0:
            self.camera.zoomIn(self.latchedCoordinates)
        else:
            self.camera.zoomOut(self.latchedCoordinates)

    def incFov(self):
        # method goes in here
        self.settings.fov *= 1.1
        log.info('Fov = %f'%self.settings.fov)
        self.fovChanged = True
        self.Refresh()
        pass


    def decFov(self):
        # method goes in here
        self.settings.fov /= 1.1
        log.info('Fov = %f'%self.settings.fov)
        self.fovChanged = True
        self.Refresh()
        pass
    

    def onKeyDown(self, event):
        assert isinstance(event, wx.KeyEvent)
        keycode = event.KeyCode
        key = None
        if event.m_controlDown:
            key = ('CTRL', ) + key
        try:
            try:
                key = chr(keycode)
            except ValueError:
                key = keycode
                
            log.info("Key, Keycode = %s, %d"%(key, keycode))
            
            key = (key, )
            if event.m_metaDown:
                key = ('META', ) + key
            if event.m_altDown:
                key = ('ALT', ) + key
            if event.m_shiftDown:
                key = ('SHIFT', ) + key
            if event.m_controlDown:
                key = ('CTRL', ) + key
            if key in self.keyBindings:
                for cb in self.keyBindings[key]:
                    cb()
        except:
            log.debug('keycode = %d'%keycode)
            
        if keycode == 27: # ESC key
            self.Close()
        
    def updateScene(self):
        self.scene.update()
    
    def Refresh(self):
        if not self.refreshAlreadyScheduled:
            wx.CallAfter(super(Canvas3D, self).Refresh)
            self.refreshAlreadyScheduled = True
    
    def onPaint(self,event):
        try:
            dc = wx.PaintDC(self)
            self.refreshAlreadyScheduled = False
            self.SetCurrent()
            if not self.init:
                glutInit()
                self.InitGL()
                self.init = 1
                self.renderStart = time()
                self.nFrames = 0
            self.updateScene()
            self.onDraw()
        except:
            traceback.print_exc(10)
            
        #self.onDrawPicking('dummy')
        #self.SwapBuffers()
        
    def onClose(self, event):
        log.info('Closing application')
        if not self.Unbind(wx.EVT_PAINT):
            log.error("Failed in unbind")
        if not self.Unbind(wx.EVT_SIZE):
            log.error("Failed in unbind")
        if not self.Unbind(wx.EVT_KEY_DOWN):
            log.error("Failed in unbind")
        self.parent.Close()
    
    def addSourceLight(self):
        glPushMatrix()
        glLoadIdentity()
        glLightfv(GL_LIGHT2, GL_DIFFUSE, (0.3, 0.3, 0.3, 1.0))
        glLightfv(GL_LIGHT2, GL_POSITION, (0, 0, 0, 0))
        glEnable(GL_LIGHT2)
        glPopMatrix()
        
    @timestat
    def onDraw(self):
        self.renderPickingTimer.Stop()
        self.pickingEnabled    = False
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        if self.fovChanged:
            self.fovChanged = False
            self.onResize('ignore')
            
        if self.cameraChanged:
            self.cameraChanged = False
            glMatrixMode(GL_MODELVIEW)
            glLoadIdentity()
            self.camera.applyTransformation()
            
        # various lights to use in the scene
        light_diffuse = [0.8, 0.8, 0.8, 1.0]
        light_ambient = [0.2, 0.2, 0.2, 1.0]
        light0_position = [1.0, 1.0, 1.0, 0.0]
        light2_position = [-1.0, 0.0, 0.0, 0.0]
        
        glMatrixMode(GL_MODELVIEW)
        
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        
        glLightfv(GL_LIGHT0, GL_DIFFUSE, COLOR_WHITE)
        glLightfv(GL_LIGHT0, GL_POSITION, light0_position)
        glLightfv(GL_LIGHT1, GL_AMBIENT, light_ambient)
        glLightfv(GL_LIGHT2, GL_DIFFUSE, COLOR_WHITE)
        glLightfv(GL_LIGHT2, GL_POSITION, light2_position)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glEnable(GL_LIGHT1)
        glEnable(GL_LIGHT2)
        
        glEnable(GL_LINE_SMOOTH)
        glEnable(GL_POINT_SMOOTH)
        glEnable(GL_MULTISAMPLE)
        #self.addSourceLight()
        
        
        glPushMatrix()
        glScalef(1.0, -1.0, 1.0)
        self.scene.render()
        glPopMatrix()
        
        # Draw the ground
        glDisable (GL_LIGHTING)
        glColor4fv(COLOR_BLACK)
        
        glBegin(GL_LINES)
        for x in range(-200, 210, 10):
            glVertex3f(x, -0.55, 200.0)
            glVertex3f(x, -0.55, -200.0)
            glVertex3f(200.0, -0.55, x)
            glVertex3f(-200.0, -0.55, x)
        glEnd()
        
        self.ground.render()
        
        glEnable (GL_LIGHTING)
        
        self.scene.render()
        self.highlightLatchedObject()
        
        self.overlay.render()
        
        self.SwapBuffers()
        
        td = time() - self.renderStart
        self.nFrames += 1
        if td > 5:
            log.info('Rendering %f fps'%(self.nFrames/td))
            self.renderStart = time()
            self.nFrames = 0
        self.renderPickingTimer.Start(10)
            
    def highlightLatchedObject(self):
        if self.latchedObject and self.latchedObject.selectable:
            glMatrixMode(GL_MODELVIEW)
            glPushMatrix()
            glLoadIdentity()
            scale = 0.999
            glScalef(scale, scale, scale)
            self.camera.applyTransformation()
            
            glPushAttrib(  GL_ENABLE_BIT
                         | GL_COLOR_BUFFER_BIT
                         | GL_LIGHTING_BIT
                         | GL_CURRENT_BIT
                         | GL_MULTISAMPLE_BIT
                         | GL_SCISSOR_BIT
                         | GL_POLYGON_BIT)
            
            #glPolygonOffset(1.0,1.0)
            #glEnable(GL_POLYGON_OFFSET_FILL)
            #glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
            
            glDisable(GL_LIGHTING)
            glShadeModel(GL_FLAT)
            glColor4f(1.0, 1.0, 1.0, 0.7)
            
            def makeHierarchy(ob):
                h = [ob]
                while hasattr(ob, 'parent'):
                    if ob.parent:
                        ob = ob.parent
                        h.append(ob)
                    else:
                        break
                h.reverse()
                return h[1:]
            
            objHierarchy = makeHierarchy(self.latchedObject)
            if objHierarchy:
                for obj in objHierarchy[:-1]:
                    if hasattr(obj, 'transforms'):
                        for t in obj.transforms:
                            t.apply()
                        
                # draw the highlight
                self.latchedObject.highlightRender()
            
            #glDisable(GL_POLYGON_OFFSET_FILL)
            glPopAttrib()
            glPopMatrix()
            
            
            if self.latchedObject.tooltip:
                log.debug('Drawing tooltip: %s'%self.latchedObject.tooltip)
                self.drawTooltip()
            
    def drawTooltip(self):
        # draw the black border
        glMatrixMode(GL_MODELVIEW)
        glPushMatrix()
        glLoadIdentity()
        
        glMatrixMode(GL_PROJECTION)
        glPushMatrix()
        glLoadIdentity()
        w, h = self.windowSize
        size = max(w, h) / 2.0
        aspect = float(w) / float(h)
        if w <= h:
            aspect = float(h) / float(w)
            glOrtho(-size, size, -size*aspect, size*aspect, -100000.0, 100000.0)
        else:
            glOrtho(-size*aspect, size*aspect, -size, size, -100000.0, 100000.0)
        glScaled(aspect, aspect, 1.0)
        
        glPushAttrib(GL_LIGHTING_BIT| GL_DEPTH_BITS)
        glDisable(GL_LIGHTING)
        glDisable(GL_DEPTH_TEST)
        
        pos = self.lastMouseMotionPosition
        glTranslate(pos[0] - w/2, h/2 - pos[1], pos[2])
        

        bbox = tooltipFont.BBox(str(self.latchedObject.tooltip))
        ipadding = 6
        opadding = 10
        
        glColor4f(0, 0, 0, 0.6)
        glBegin(GL_TRIANGLE_STRIP)
        glVertex3f(opadding, opadding, 0)
        glVertex3f(bbox[3] + 2*ipadding + opadding, opadding, 0)
        glVertex3f(opadding, bbox[4] + 2*ipadding + opadding, 0)
        glVertex3f(bbox[3] + 2*ipadding + opadding, bbox[4] + 2*ipadding + opadding, 0)
        glEnd()

        glTranslate(ipadding + opadding, ipadding + opadding, 0)
        glColor4f(1, 1, 1, 1)
        tooltipFont.Render(self.latchedObject.tooltip)
        
        glPopAttrib()

        glMatrixMode(GL_MODELVIEW)
        glPopMatrix()
        
        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        # draw the text
        pass
        
    def onDrawPicking(self, event):
        self.renderPickingTimer.Stop()
        
        glPushAttrib(  GL_ENABLE_BIT
                     | GL_COLOR_BUFFER_BIT
                     | GL_LIGHTING_BIT
                     | GL_CURRENT_BIT
                     | GL_MULTISAMPLE_BIT
                     | GL_SCISSOR_BIT)
        
        glDisable(GL_LIGHTING)
        glDisable(GL_BLEND)
        glDisable(GL_LINE_SMOOTH)
        glDisable(GL_POINT_SMOOTH)
        glDisable(GL_COLOR_MATERIAL)
        glDisable(GL_MULTISAMPLE)

        glShadeModel(GL_FLAT)
        
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        
        glMatrixMode(GL_MODELVIEW)
        glPushMatrix()

        self.ground.renderPicking()
        self.scene.renderPicking()

        glPopMatrix()
        glPopAttrib()
        
        self.pickingEnabled = True
        
    def pick(self, x, y):
        rgb = glReadPixels(x, y, 1, 1, GL_RGB,
                               GL_UNSIGNED_BYTE, outputType=None)
        rgb = rgb[0][0]
        val = rgb[0]*256*256 + rgb[1]*256 + rgb[2]
        
        depth = glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
        depth = depth[0][0]
        self.pickDepth = depth
        coords = gluUnProject(x, y, depth)
        log.debug('Pick val, coords = 0x%06x, %5.4f, %5.4f, %5.4f'%(
                        val, coords[0], coords[1], coords[2]))
        return val, coords
        
    def onResize(self, event):
        self.setProjection()

    def setProjection(self, size=None):
        if not size:
            size = self.windowSize = self.GetClientSizeTuple()
            
        glViewport(0, 0, size[0], size[1])
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(self.settings.fov, float(size[0])/size[1], self.settings.nearZ, self.settings.farZ)
        self.camera.PMatrix  = glGetDoublev(GL_PROJECTION_MATRIX)
        
    def onCameraChanged(self):
        self.cameraChanged = True
        self.latchedObject = self.oldLatchedObject = None
        self.Refresh()
        
    def InitGL(self):
        self.InitFonts()
        glEnable(GL_DEPTH_TEST)
        glClearColor(*BACKGROUNDCOLOR)
        glClearDepth(1.0)

        self.setProjection()

        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        self.camera.applyTransformation()
        
    def InitFonts(self):
        log.info('Initializing fonts information')
        
# TODO - gui to change settings
# TODO - specular lighting effects and simulate window pane
# TODO - make gears, pipes, etc
# TODO - implement selecting objects
# TODO - add capability for stereo view
# TODO - add capability for dynamic objects based on distance
# TODO - overlay items
                  
def main():
    app = wx.App()
    frame3D = wx.Frame(None, -1, "Canvas 3D", wx.Point(1100, 50), wx.Size(800,400))
    canvas = Canvas3D(frame3D, WX_GL_RGBA, WX_GL_DOUBLEBUFFER)
    frame3D.Show()
    app.MainLoop()
    print "Exiting main loop\n"
    timestat.printStats()

    
if __name__ == '__main__':
    main()
        
    
